<?php namespace comb;

$_COMB = array();


define(__NAMESPACE__ . '\METHOD',  strtolower($_SERVER['REQUEST_METHOD']));
define(__NAMESPACE__ . '\URI',     $_SERVER['REQUEST_URI']);
//define(__NAMESPACE__ . '\PATH',    current(explode('?', $_SERVER['REQUEST_URI'])));
define(__NAMESPACE__ . '\PATH',    strstr($_SERVER['REQUEST_URI'], '?', true) ?: $_SERVER['REQUEST_URI']);
define(__NAMESPACE__ . '\SCRIPT',  $_SERVER['SCRIPT_NAME']);
define(__NAMESPACE__ . '\DOCROOT', DIRECTORY_SEPARATOR . trim($_SERVER['DOCUMENT_ROOT'], ' ' . DIRECTORY_SEPARATOR));
define(__NAMESPACE__ . '\ISXHR',   isset($_SERVER['HTTP_X_REQUESTED_WITH']) && ($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'));

function init($options = array())
{
    global $_COMB;
    
    /**
     * if "config.php" exists in the docroot, assume it's an array of
     * values that will override the $default_options array (above)
     */
    $comb_config = array();
    $comb_config_file = DOCROOT . DIRECTORY_SEPARATOR . 'config.php';
    if (file_exists($comb_config_file)) {
        require $comb_config_file;
    }
    
    $_COMB['config'] = array_merge(get_default_config(), $comb_config);


    $_COMB['vars'] = array();           // vars extracted into template scope
    $_COMB['buffer'] = array();         // rendered template storage
    $_COMB['headers'] = array();        // user-specified headers, sent by comb\send()
    $_COMB['templates'] = array();      // auto-detected templates
    $_COMB['user_templates'] = array(); // user-specified templates
    $_COMB['method_chains'] = array();  // arrays of method functions


    /**
     * My assumption is that XHR requests are pretty minimal, either
     * resulting in a simple JSON response, or maybe just a redirect, in 
     * which case there's not much need to bother with buffering. Might 
     * need to create a special config value to override this behavior
     * in case somebody wants to use the buffer in their XHR response. For
     * whatever reason. Until then, turn off buffering and rendering for XHRs
     */
    if (ISXHR) {
        $_COMB['config']['enable_output_buffer'] = false;
        $_COMB['config']['render_output'] = false;
    }
}

function get_default_config()
{
    return array(
        'template_dir'          => '.',
        'template_ext'          => '.tpl.php',
        'template_detection'    => true,

        'layout_filename'       => 'layout',
        'layout_detection'      => true,

        'enable_output_buffer'  => true,
        'render_output'         => true,
        'render_output_get'     => true,
        'render_output_post'    => false,
        'render_output_xhr'     => false,
        
        'prepend_filename'      => 'prepend.php',
        'append_filename'       => 'append.php',
        'halt_execution'        => false,
        
        'debug_level'           => 0,               // no logging
        'error_log'             => '',              // use PHP's default log
    );
}

function ifset($key, $default_value, $strict = FALSE)
{
    if ($strict) {
        return !isset($_REQUEST[$key]) ? $default_value : $_REQUEST[$key];
    }

    return empty($_REQUEST[$key]) ? $default_value : $_REQUEST[$key];
}


function ini_set($name, $value)
{
    global $_COMB;
    $_COMB['config'][$name] = $value;
}

function ini_get($name)
{
    global $_COMB;
    return $_COMB['config'][$name];
}


/**
 * assign a template variable 
 */
function assign($key, $value)
{
    global $_COMB;
    $_COMB['vars'][$key] = $value;
}

/**
 * fetch the value of a previously set template variable
 */
function fetch($key)
{
    global $_COMB;
    return isset($_COMB['vars'][$key]) ? $_COMB['vars'][$key]: null;
}

function log($message = null, $append_newline = true, $prepend_timestamp = true)
{
    if (empty($message)) return;

    global $_COMB;
    
    $destination = $_COMB['config']['error_log'];

    // see "message_type" param - http://php.net/manual/en/function.error-log.php
    $message_type = 0;
    if (!empty($destination)) {
        $destination = realpath(DOCROOT . DIRECTORY_SEPARATOR . $destination);
        if (is_writable($destination)) {
            $message_type = 3;
            
            // only append newlines for custom logs.
            if ($append_newline) {
                $message .= "\n";
            }
            
            if ($prepend_timestamp) {
                $message = date('Y/m/d H:i:s - ', time()) . $message;
            }
        }
    }
        
    error_log($message, $message_type, $destination);
}

/**
 * add a template to the userland queue (as opposed to the auto-detected queue of templates)
 */
function queue($template_file, $buffer_name = '__content')
{
    global $_COMB;
    $_COMB['user_templates'][] = array('file' => $template_file, 'name' => $buffer_name);
}

/**
 * alias to queue()
 */
function add_template($template_file, $buffer_name = '__content')
{
    queue($template_file, $buffer_name);
}

/**
 * fetch the content that was rendered into the named buffer
 */
function buffer($name = '__content')
{
    global $_COMB;
    return ($_COMB['config']['enable_output_buffer'] && !empty($_COMB['buffer'][$name])) ? $_COMB['buffer'][$name]: null;
}

function filter()
{
    global $_COMB;
    
    $req_path_dirs = explode('/', trim(dirname(SCRIPT), ' /'));
    $current_dir = '';
    
    /**
     * arrays that will hold any detected filter (prepend/append) scripts
     */
    $prepends = array();
    $appends  = array();    
    
    
    /**
     * 1) start at directory of the requested file
     * 2) if there's a filter script, store it to the appropriate array
     * 3) if we're not in the document root, back up one directory, goto step 1
     *
     * i.e. a request for "/path/to/some/script.php" will find filters in:
     *  {DOCROOT}/path/to/some/
     *  {DOCROOT}/path/to/
     *  {DOCROOT}/path/
     *  {DOCROOT}/
     */
    $dir = SCRIPT;
    do {
        $dir = dirname($dir);
    
        if ($dir == '/') $dir = '';
        
        $prepend_file = DOCROOT . $dir . '/' . $_COMB['config']['prepend_filename'];
        if (file_exists($prepend_file)) {
            array_unshift($prepends, $prepend_file);
        }
        
        $append_file = DOCROOT . $dir . '/' . $_COMB['config']['append_filename'];
        if (file_exists($append_file)) {
            $appends[] = $append_file;
        }
        
    } while (!empty($dir));
    
    if ($_COMB['config']['enable_output_buffer']) ob_start();
    
    /**
     * run any pre-filters (working from docroot DOWN the directory tree)
     */
    foreach ($prepends as $prepend) {
        if (halted()) {
            return false;
        }
        include $prepend;
    }
    
    /**
     * ...run the requested script
     */
    if (halted()) {
        return false;
    }
    
    include $_SERVER['SCRIPT_FILENAME'];


    /**
     * ...and run any post-filters (heading back UP the dir tree)
     */
    foreach ($appends as $append) {
        if (halted()) {
            return false;
        }
        include $append;
    }
    
    if ($_COMB['config']['enable_output_buffer']) {
        $_COMB['buffer']['__content'] = ob_get_clean();
    }
    
    return true;
}

function execute()
{
    switch (ini_get('debug_level')) {
        case 1: 
            log('EXECUTE: beginning');
            break;
    }

    global $_COMB;
    
    if (halted()) {
        switch (ini_get('debug_level')) {
            case 1: 
                log('EXECUTE: halt detected, returning to bootstrap');
                break;
        }
        
    
        return false;
    }
    
    /**
     * run any closures that have been added to the method chain for this
     * particular request. if it's an XHR, but no 'xhr' methods are defined,
     * then just run the actual request method chain (POST or GET probably)
     */
    $method = (ISXHR && !empty($_COMB['method_chains']['xhr'])) ? 'xhr': METHOD;

    if (!empty($_COMB['method_chains'][$method])) {
        foreach ($_COMB['method_chains'][$method] as $func) {
            $func();
        }
    }
    
    return true;
}

/**
 *  set the flag to indicate that filter inclusion should be halted asap.
 */
function halt()
{
    global $_COMB;
    $_COMB['config']['halt_execution'] = true;
}

function halted()
{
    global $_COMB;
    return $_COMB['config']['halt_execution'];
}

/**
 * 302 redirect is pretty common...
 */
function redirect($url, $code = 302)
{
    global $_COMB;
    
    halt();
    header("Location: $url", true, $code);
    $_COMB['config']['render_output'] = false;
    
    switch (ini_get('debug_level')) {
        case 1: 
            log('redirecting to ' . $url);
            break;
    }
}

function add_header($name, $value)
{
    global $_COMB;
    $_COMB['headers'][$name] = $value;
}

function header($string, $replace = true, $http_response_code = null)
{
    global $_COMB;
    $_COMB['headers'][] = array(
        'string'             => $string, 
        'replace'            => $replace, 
        'http_response_code' => $http_response_code
    );

}


/**
 * Add a request handler (closure) into the specified method chain
 */
function handle($method, $closure)
{
    global $_COMB;
    $_COMB['method_chains'][strtolower($method)][] = $closure;
}

function get($closure, $render_output = true)
{
//    ini_set('render_output', $render_output);
    handle('get', $closure);
}

function post($closure, $render_output = false)
{
//    ini_set('render_output_post', $render_output);
    handle('post', $closure);
}

function put($closure, $render_output = false)
{
//    ini_set('render_output_put', $render_output);
    handle('put', $closure);
}

function delete($closure, $render_output = false)
{
//    ini_set('render_output_delete', $render_output);
    handle('delete', $closure);
}

function xhr($closure, $render_output = false)
{
//    ini_set('render_output_xhr', $render_output);
//    ini_set('render_output', $render_output);
    handle('xhr', $closure);
}

function render_output($render = true)
{
    ini_set('render_output', $render);
}

function render()
{
    /**
     * render templates
     *  1) if template auto-detect is on, then try to find and include:
     *      TPLDIR/path/to/<script>.tpl.php   
     *      TPLDIR/path/to/layout.tpl.php
     *      TPLDIR/path/layout.tpl.php
     *      TPLDIR/layout.tpl.php
     *
     *      (using $_COMB['output'] as the buffer in each successive template)
     */

    global $_COMB;

    if (!$_COMB['config']['render_output']) {
        return false;
    }
    
    /**
     * check if any individual method's rendering is disabled
     * e.g. render_output_get, render_output_post, etc
     */
    if (empty($_COMB['config']['render_output_' . METHOD]) ) {
        return false;
    }        

    
    
    if ($_COMB['config']['template_dir'][0] == DIRECTORY_SEPARATOR) {
        // absolute path to a different template directory
        $template_dir = realpath($_COMB['config']['template_dir']);
    } else {
        // template dir is relative to site's document root
        $template_dir = realpath(DOCROOT . DIRECTORY_SEPARATOR . $_COMB['config']['template_dir']);
    }
    
    if ($template_dir === false) {
        die("template dir not found!");
    }
    
    if ($_COMB['config']['template_detection']) {
        /**
         * find layout templates starting in template root, working down to the 
         * current script's directory
         */
        $segments = explode(DIRECTORY_SEPARATOR, dirname(SCRIPT));
        $segments = (empty($segments[0]) && empty($segments[1])) ? array(''): $segments;

        $dir = $template_dir;
        foreach ($segments as $segment) {
            $dir .= $segment . DIRECTORY_SEPARATOR;            
            $layout_file = $dir . $_COMB['config']['layout_filename'] . $_COMB['config']['template_ext'];

            /**
             * push found layout template (if found) into the front of the 
             * template array, so the outer most layout will be the last one 
             * rendered
             */
            if (file_exists($layout_file)) {
                array_unshift($_COMB['templates'], array('file' => $layout_file, 'name' => '__content'));
            }

        }

        /**
         * try to include the template of the same name
         *  e.g., index.php => index.tpl.php
         *  if it exists, push it onto the front of the template queue with 
         * default name "__content"
         */
        $template_file = realpath($template_dir 
            . dirname(SCRIPT) . DIRECTORY_SEPARATOR
            . basename(SCRIPT, '.php') 
            . $_COMB['config']['template_ext']);
            
        if (file_exists($template_file)) {
            array_unshift($_COMB['templates'],  array('file' => $template_file, 'name' => '__content'));
        }
    }

    /**
     * add any the auto-detected templates (if they exist) onto the end of the
     * user-specified templates (if they exist)
     */
    $_COMB['templates'] = array_merge($_COMB['user_templates'], $_COMB['templates']);

    
    /**
     * loop through the template queus and include each one, storing their 
     * rendered contents to their specified buffers
     */
    extract($_COMB['vars']);

    foreach ($_COMB['templates'] as $_template) {
        if ($_COMB['config']['enable_output_buffer']) {
            ob_start();
        }

        include $_template['file'];
        
        if ($_COMB['config']['enable_output_buffer']) {
            $_COMB['buffer'][$_template['name']] = ob_get_clean();
        }
    }

    return true;
}


function send()
{
    global $_COMB;
    
    switch (ini_get('debug_level')) {
        case 1:

            break;
    }
    
    /**
     * if buffering is disabled, then headers have probably already been sent.
     * if they haven't at this point, something's probably wrong because all
     * the scripts have already been run :/
     */
    if ($_COMB['config']['enable_output_buffer']) {
        foreach ($_COMB['headers'] as $h) {
            \header($h['string'], $h['replace'], $h['http_response_code']);
        }
    
        echo $_COMB['buffer']['__content'];
    }
    
    if (ini_get('debug_level') == 1) {
        echo "\n\n<!-- time:" . xdebug_time_index() . " -->";
    }
    
    // we exit here so that the requested script doesn't get run, since we 
    // already included it during the comb_run() process.
    exit;
}
